// Copyright (c) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information.
#include "pch.h"
#include <http_msg.h>
#include "ExceptionPolicyFactory.h"
#include "ForecastManager.h"
#include "ThumbnailManager.h"
#include "WebException.h"
#include "WWOWeatherService.h"

using namespace Weathr;

using namespace concurrency;
using namespace Platform;
using namespace Platform::Collections;
using namespace std;
using namespace web;
using namespace Windows::Foundation;
using namespace Windows::Foundation::Collections;
using namespace Windows::Globalization;
using namespace Windows::Globalization::DateTimeFormatting;
using namespace Windows::Storage;

// Distance covered by a single forecast group.
const int32_t GroupToleranceInFeet = 150 * 5280; // 100 miles

ForecastManager^ ForecastManager::m_current;

ForecastManager::ForecastManager()
    : m_forecastGroups(ref new Vector<ForecastGroup^>())
    , m_exceptionPolicy(ExceptionPolicyFactory::GetCurrentPolicy())
    , m_weatherService(new (nothrow) Web::WWOWeatherService())
    , m_calendar(ref new Vector<DateTime, equal_to>())
{
    auto c = ref new Windows::Globalization::Calendar();
    c->ChangeCalendarSystem(Windows::Globalization::CalendarIdentifiers::Gregorian);
    c->SetToNow();
    auto year = c->Year;
    auto month = c->Month;
    auto day = c->Day;
    c = ref new Windows::Globalization::Calendar();
    c->ChangeCalendarSystem(Windows::Globalization::CalendarIdentifiers::Gregorian);
    c->Year = year;
    c->Month = month;
    c->Day = day;
    c->Period = 1;
    c->Hour = c->FirstHourInThisPeriod;
    c->Minute = 0;
    c->Second = 0;
    c->Nanosecond = 0;

    auto date = c->GetDateTime();
    CurrentDate = date;
    PreferredDate = date;
    m_minDate.UniversalTime = 0;
    m_maxDate.UniversalTime = 0;
}

String^ ForecastManager::LastRefreshTime::get()
{
    auto dateFormatter = ref new DateTimeFormatter(YearFormat::Full, MonthFormat::Full, DayFormat::Default, DayOfWeekFormat::None);
    auto timeFormatter = ref new DateTimeFormatter(HourFormat::Default, MinuteFormat::Default, SecondFormat::Default);
    auto c = ref new Windows::Globalization::Calendar();
    c->SetToNow();
    auto dateTime = c->GetDateTime();
    String^ date = dateFormatter->Format(dateTime);
    String^ time = timeFormatter->Format(dateTime);

    wchar_t buffer[1024] = { L'\0' };
    swprintf_s(buffer, L"Page last updated %s %s", date->Data(), time->Data());
    return ref new String(buffer);
}

Forecast^ ForecastManager::FindForecast(String^ uid)
{
    for (auto group : ForecastGroups)
    {
        for (auto forecast : group->Items)
        {
            if (forecast->UniqueIdentifier == uid)
            {
                return forecast;
            }
        }
    }
    return nullptr;
}

Forecast^ ForecastManager::FindForecast(const LatLon& location, double toleranceFeet)
{
    vector<Forecast^> candidates;
    for (auto group : ForecastGroups)
    {
        for (auto forecast : group->Items)
        {
            if (DistanceInFeet(forecast->Latlon, location) <= toleranceFeet)
            {
                candidates.emplace_back(forecast);
            }
        }
    }
    return candidates.size() == 0 ? nullptr : *min_element(begin(candidates), end(candidates), [location](Forecast^ f1, Forecast^ f2)
    {
        return DistanceInFeet(f1->Latlon, location) < DistanceInFeet(f2->Latlon, location);
    });
}

void ForecastManager::LoadLocations(const json::value& v)
{
    // Forecast and ForecastGroup objects must be created on the ASTA thread.
    assert(IsMainThread());

    if (v.cbegin()->first.as_string() == L"locations")
    {
        const json::value& values = v.cbegin()->second;
        for (auto iter = values.cbegin(); iter != values.cend(); ++iter)
        {
            const json::value& obj = iter->second;

            LatLon location;
            utility::string_t locality;
            utility::string_t adminDistrict;
            utility::string_t countryRegion;
            for (auto iter = obj.cbegin(); iter != obj.cend(); ++iter)
            {
                auto name = iter->first.as_string();
                if (name == L"locality")
                {
                    locality = iter->second.as_string();
                }
                if (name == L"adminDistrict")
                {
                    adminDistrict = iter->second.as_string();
                }
                if (name == L"countryRegion")
                {
                    countryRegion = iter->second.as_string();
                }
                else if (name == L"latitude")
                {
                    location.Latitude = static_cast<float>(iter->second.as_double());
                }
                else if (name == L"longitude")
                {
                    location.Longitude = static_cast<float>(iter->second.as_double());
                }
            }
            auto forecast = AddForecast(location, ref new String(locality.c_str()), ref new String(adminDistrict.c_str()), ref new String(countryRegion.c_str()));
        }
    }
}

Forecast^ ForecastManager::AddForecast(const LatLon& location, String^ locality, String^ adminDistrict, String^ countryRegion)
{
    Forecast^ newForecast = ref new Forecast(location);
    newForecast->Locality = locality;
    newForecast->AdminDistrict = adminDistrict;
    newForecast->CountryRegion = countryRegion;

    auto findResult = FindForecast(location, 50 * 5280);
    if (findResult != nullptr && findResult->Locality == locality)
    {
        return findResult;
    }
    UpdateForecastDate(newForecast, CurrentDate);

    // Check for the thumbnail image for this location.
    ThumbnailManager::GetThumbnailAsync(newForecast).then([this, newForecast](StorageFile^ file)
    {
        assert(IsMainThread());

        // If the thumbnail file doesn't exist, queue it for creation.
        if (file == nullptr)
        {
            return CreateThumbnailAsync(newForecast);
        }
        newForecast->ThumbnailImagePath = file->Path;
        return task_from_result();

    }).then([](task<void> previousTask)
    {
        try
        {
            previousTask.get();
        }
        catch (AccessDeniedException^)
        {
            // Race condition: another task was deleting the thumbnail as the new one was being created.
        }

    }).then(ObserveException<void>(m_exceptionPolicy));

    for (auto forecastGroup : m_forecastGroups)
    {
        for (auto forecast : forecastGroup->Items)
        {
            if (DistanceInFeet(location, forecast->Latlon) <= GroupToleranceInFeet)
            {
                forecastGroup->Append(newForecast);
                ForecastAdded(newForecast);
                return newForecast;
            }
        }
    }
    auto group = ref new ForecastGroup();
    group->Append(newForecast);
    m_forecastGroups->Append(group);
    ForecastAdded(newForecast);
    return newForecast;
}

task<void> ForecastManager::CreateThumbnailAsync(Forecast^ forecast)
{
    // Create file.
    return ThumbnailManager::CreateThumbnailAsync(forecast).then([this](StorageFile^ file)
    {
        assert(IsBackgroundThread());
        return file->OpenAsync(FileAccessMode::ReadWrite);

    }, task_continuation_context::use_arbitrary()).then([this, forecast](Windows::Storage::Streams::IRandomAccessStream^ stream)
    {
        assert(IsBackgroundThread());
        task_completion_event<void> tce;

        // After the thumbnail image is created, write it to disk.
        create_task(tce).then([this, forecast, stream]()
        {
            assert(IsBackgroundThread());
            return ThumbnailManager::WriteThumbnailAsync(forecast, stream);

        }).then(ObserveException<void>(m_exceptionPolicy));

        m_thumbnailUpdateQueue.push(make_tuple(forecast, stream, tce));

    }, task_continuation_context::use_arbitrary());
}

void ForecastManager::RemoveForecast(Forecast^ forecast)
{
    for (unsigned int i = 0; i < ForecastGroups->Size; ++i)
    {
        auto group = ForecastGroups->GetAt(i);
        for (unsigned int j = 0; j < group->Items->Size; ++j)
        {
            if (forecast == group->Items->GetAt(j))
            {
                group->Items->RemoveAt(j);

                // Delete the item's thumbnail.
                forecast->ThumbnailImagePath = "";
                ThumbnailManager::GetThumbnailAsync(forecast).then([](StorageFile^ file)
                {
                    if (file != nullptr)
                    {
                        return create_task(file->DeleteAsync());
                    }
                    return task_from_result();

                }).then(ObserveException<void>(m_exceptionPolicy));

                if (group->Items->Size == 0)
                {
                    ForecastGroups->RemoveAt(i);
                }
                return;
            }
        }
    }
}

task<void> ForecastManager::CheckinAsync(Forecast^ forecast, const utility::datetime& now)
{
    assert(IsMainThread());

    forecast->LastCheckin = now;
    forecast->IsPending = true;
    forecast->Current = nullptr;
    forecast->UpdateFailed = false;

    task_completion_event<bool> evt = Logger::LogAsyncAction(this, "Checking in for " + forecast->Locality + " (" + forecast->Latitude + " , " + forecast->Longitude + ")...");

    return run_async_with_retry<void>([this, forecast]()
    {
        return m_weatherService->GetWeatherInfoAsync(forecast);

    }, [](uint32_t retryCount)
    {
        // Retry up to 2 times.
        if (retryCount < 2u)
        {
            // For the case of an HTTP request, create a delay before returning.
            return create_delayed_task(std::chrono::seconds(1), []{ return true; });
        }
        else
        {
            // Otherwise, cancel processing as soon as possible.
            return task_from_result(false);
        }

    }).then([this, forecast, evt](task<void> previousTask)
    {
        assert(IsMainThread());

        UpdateForecastDate(forecast, CurrentDate);
        OnPropertyChanged("LastRefreshTime");
        forecast->IsPending = false;

        try
        {
            forecast->UpdateFailed = true;
            previousTask.get();
            forecast->UpdateFailed = false;
            evt.set(true);
        }
        catch (const web::http::http_exception&)
        {
            // Possibly no network conection.
            evt.set(false);
        }
        catch (const Web::WebConnectionException&)
        {
            // Received a response body, but the body contained an error code.
            evt.set(false);
        }

    }, task_continuation_context::use_current());
}

void ForecastManager::UpdateCalendarRange(DateTime minDate, DateTime maxDate)
{
    if (m_minDate.UniversalTime == minDate.UniversalTime && m_maxDate.UniversalTime == maxDate.UniversalTime)
    {
        return;
    }

    m_calendar->Clear();
    if (minDate.UniversalTime == 0 && maxDate.UniversalTime == 0)
    {
        return;
    }

    auto c = ref new Windows::Globalization::Calendar();
    c->SetDateTime(minDate);
    while (c->GetDateTime().UniversalTime <= maxDate.UniversalTime)
    {
        m_calendar->Append(c->GetDateTime());
        c->AddDays(1);
    }
    m_minDate = minDate;
    m_maxDate = maxDate;

    // If the current date fell below the minimum, select the minimum.
    if (CurrentDate.UniversalTime < m_minDate.UniversalTime)
    {
        CurrentDate = m_minDate;
    }
    // If the current date is above the maximum, select the maximum.
    else if (CurrentDate.UniversalTime > m_maxDate.UniversalTime)
    {
        CurrentDate = m_maxDate;
    }
    // If we've fallen back into the range that supports the user's preferred date, set it back.
    else if (CurrentDate.UniversalTime != PreferredDate.UniversalTime)
    {
        CurrentDate = PreferredDate;
    }
}

void ForecastManager::UpdateForecastDates(DateTime date)
{
    for (auto forecast : GetAllForecasts())
    {
        UpdateForecastDate(forecast, date);
    }
}

void ForecastManager::UpdateForecastDate(Forecast^ forecast, DateTime date)
{
    for (auto data : forecast->Items)
    {
        if (data->Date.UniversalTime == date.UniversalTime)
        {
            forecast->Current = data;
            break;
        }
    }
}

vector<Forecast^> ForecastManager::SortAlphabetically(const vector<Forecast^>& forecasts)
{
    auto v = forecasts;
    sort(begin(v), end(v), [](Forecast^ f, Forecast^ g)
    {
        auto l1 = f->DisplayName;
        auto l2 = g->DisplayName;
        if (l1 == nullptr && l2 == nullptr)
        {
            return false;
        }
        return wcscmp(l1->Data(), l2->Data()) < 0;
    });
    return v;
}

vector<Forecast^> ForecastManager::GetAllForecasts()
{
    vector<Forecast^> result;
    for (auto group : ForecastGroups)
    {
        auto forecasts = group->Items;
        copy(begin(forecasts), end(forecasts), back_inserter(result));
    }
    return result;
}

vector<Forecast^> ForecastManager::GetVisibleForecasts()
{
    vector<Forecast^> result;
    for (auto group : ForecastGroups)
    {
        auto forecasts = group->Items;
        copy_if(begin(forecasts), end(forecasts), back_inserter(result), [](Forecast^ forecast)
        {
            return forecast->IsVisible;
        });
    }
    return result;
}
